peripheral.find("modem", rednet.open) 
local properties, system
local protocol, request_protocol = "CBCNetWork", "CBCcenter"
local BLACK = 0x000000FF
local RETICLE = 0x00FF00FF  
local TEXT = 0x00FFFFFF     
local RED = 0xFF0000FF
local ORANGE = 0xFFA500FF
local milDotSpacing = 20  
local maxMils = 10  ---最大刻度
local distanceFromViewer = 0.5 
local tickLength = 10
system = {
    fileName = "dat", 
    file = nil,
}

system.init = function()
    system.file = io.open(system.fileName, "r")
    if system.file then
        local tmpProp = textutils.unserialise(system.file:read("a"))
        properties = system.reset()
        for k, v in pairs(properties) do
            if tmpProp[k] then
                properties[k] = tmpProp[k]
            end
        end
        system.file:close()
    else
        properties = system.reset()
    end
end

system.reset = function()
    return {
        Name = "Co1v1",         
        Exchange = false,         
        controlCenterId = "-1",     
        INPUT = "back",          
        fire = "top",               
        cannonOffset = { x = 0, y = 3, z = 0 }, 
        minPitchAngle = -60,
        face = "west",              
        CannonYAW = "0",           
        Cannonpitch = "0",         
        password = "123456",        
        InvertYaw = false,         
        InvertPitch = false,       
        lock_yaw_range = "0",      
        lock_yaw_face = "east",    
        velocity = "160",           
        barrelLength = "8",         
        forecast = "32",            
        gravity = "0.05",           
        drag = "0.01", 
        curvedfire = false,
        camera = false,
        Lower_hanging = false,
        Auto = false
    }
end
local ho = peripheral.find("hologram")
local ca = peripheral.find("camera")
local gears = {peripheral.find("servo")}
local y = gears[1]
local p = gears[2]

system.write = function(file, obj)
    local dataToSave = {}
    for k, v in pairs(obj) do
        if type(v) == "table" then
            dataToSave[k] = {}
            for sk, sv in pairs(v) do
                dataToSave[k][sk] = (sv == nil or sv == "") and "0" or sv
            end
        else
            dataToSave[k] = (v == nil or v == "") and "0" or v
        end
    end
    
    system.file = io.open(file, "w")
    if system.file then
        system.file:write(textutils.serialise(dataToSave))
        system.file:close()
        return true
    end
    return false
end

system.init() 

if not p or not y then
    printError("Need peripheral")
else
    p.setTargetValue(0)
    y.setTargetValue(0)
    sleep(0.1)
end   

-----------function------------
local function calculateRelativePosition(yaw, pitch)
    local yawRad = math.rad(yaw)
    local pitchRad = math.rad(pitch)
    local relX = distanceFromViewer * math.cos(pitchRad) * math.sin(yawRad)
    local relY = distanceFromViewer * math.sin(pitchRad) 
    local relZ = distanceFromViewer * math.cos(pitchRad) * math.cos(yawRad)
    return relX, relY, relZ
end

local quatMultiply = function(q1, q2)
    local newQuat = {}
    newQuat.w = -q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + q1.w * q2.w
    newQuat.x = q1.x * q2.w + q1.y * q2.z - q1.z * q2.y + q1.w * q2.x
    newQuat.y = -q1.x * q2.z + q1.y * q2.w + q1.z * q2.x + q1.w * q2.y
    newQuat.z = q1.x * q2.y - q1.y * q2.x + q1.z * q2.w + q1.w * q2.z
    return newQuat
end

local RotateVectorByQuat = function(quat, v)
    local x = quat.x * 2
    local y = quat.y * 2
    local z = quat.z * 2
    local xx = quat.x * x
    local yy = quat.y * y
    local zz = quat.z * z
    local xy = quat.x * y
    local xz = quat.x * z
    local yz = quat.y * z
    local wx = quat.w * x
    local wy = quat.w * y
    local wz = quat.w * z
    local res = {}
    res.x = (1.0 - (yy + zz)) * v.x + (xy - wz) * v.y + (xz + wy) * v.z
    res.y = (xy + wz) * v.x + (1.0 - (xx + zz)) * v.y + (yz - wx) * v.z
    res.z = (xz - wy) * v.x + (yz + wx) * v.y + (1.0 - (xx + yy)) * v.z
    return res
end

local negaQ = function(q)
    return {
        w = q.w,
        x = -q.x,
        y = -q.y,
        z = -q.z
    }
end

local newVec = function()
    return {
        x = 0,
        y = 0,
        z = 0
    }
end

function vector.scale(v, s)
    return {
        x = v.x * s,
        y = v.y * s,
        z = v.z * s
    }
end

local copysign = function(num1, num2)
    num1 = math.abs(num1)
    num1 = num2 > 0 and num1 or -num1
    return num1
end

local genParticle = function(x, y, z)
    commands.execAsync(string.format("particle electric_spark %0.6f %0.6f %0.6f 0 0 0 0 0 force", x, y, z))
end
local genHitPredictionCircle = function(x, y, z, particleCount, radius, yOffset, getname)
    local count = particleCount or 20  
    local circleRadius = radius or 0.5  
    local offset = (math.random() * 0.4) - 0.2
    local yOff = (yOffset + offset) or 0.1         
    for i = 1, count do
        local angle = math.rad( (i / count) * 360 )
        local particleX = x - circleRadius * math.cos(angle) 
        local particleZ = z - circleRadius * math.sin(angle) 
        local particleY = y + yOff
        if particleX ~= nil and particleY ~= nil and particleZ ~= nil then
            particleX = particleX 
            particleY = particleY 
            particleZ = particleZ 
            commands.execAsync(string.format("particle %s %0.6f %0.6f %0.6f 0 0 0 0 0 force", getname, particleX, particleY, particleZ))
        end
    end
end

local genStr = function(s, count)
    local result = ""
    for i = 1, count, 1 do
        result = result .. s
    end
    return result
end

local rayCaster = {
    block = nil
}

rayCaster.run = function(start, v3Speed, range, showParticle)
    local vec = {}
    vec.x = start.x
    vec.y = start.y
    vec.z = start.z
    for i = 0, range, 1 do
        vec.x = vec.x + v3Speed.x
        vec.y = vec.y + v3Speed.y
        vec.z = vec.z + v3Speed.z

        rayCaster.block = coordinate.getBlock(vec.x, vec.y, vec.z - 1)
        if (rayCaster.block ~= "minecraft:air" or i >= range) then
            break
        end
        if showParticle then
            genParticle(vec.x, vec.y, vec.z)
        end
    end
    return { x = vec.x, y = vec.y, z = vec.z, name = rayCaster.block }
end

local getCannonPos = function()
    local wPos = ship.getWorldspacePosition()
    local yardPos = ship.getShipyardPosition()
    local selfPos = coordinate.getAbsoluteCoordinates()

    local offset = {
        x = yardPos.x - selfPos.x - 0.5 - properties.cannonOffset.x,
        y = yardPos.y - selfPos.y - 0.5 - properties.cannonOffset.y,
        z = yardPos.z - selfPos.z - 0.5 - properties.cannonOffset.z
    }
    offset = RotateVectorByQuat(ship.getQuaternion(), offset)
    return {
        x = wPos.x - offset.x,
        y = wPos.y - offset.y,
        z = wPos.z - offset.z
    }
end

function math.lerp(a, b, t)
    return a + (b - a) * t
end

local ln = function(x)
    return math.log(x) / math.log(math.exp(1))
end

local function getCoyawByDistance(dis)
    if dis == nil then
        return 0.04
    end
    dis = math.max(0, math.min(dis, 1500))
    local coyaw 
    if dis <= 200 then
        coyaw = 0.04 - 0.0001 * dis
    end

    if dis > 200 and dis <= 800 then
        coyaw = 0.02 - 0.000016 * (dis - 200)
    end

    if dis > 800 then
        coyaw = 0.01 - 0.0000057 * (dis - 800)
    end

    return math.max(coyaw, 0.005)
end

local pitchList = {}
local Listrad = math.rad(90)
local startRad = -Listrad  -- -90
local endRad = Listrad     -- 90
local stepRad = math.rad(0.01)  -- 步长 0.01度转换为弧度
local currentRad = startRad
while currentRad <= endRad do
    table.insert(pitchList, currentRad)
    currentRad = currentRad + stepRad
end
local minRad = math.rad(45)  
local maxRad = math.rad(90)  
local validPitchList = {}
for _, pitchRad in ipairs(pitchList) do
    if pitchRad >= minRad and pitchRad <= maxRad then
        table.insert(validPitchList, pitchRad)
    end
end

local getTime = function(dis, pitch)
    local barrelLength = #properties.barrelLength == 0 and 0 or tonumber(properties.barrelLength)
    barrelLength = barrelLength and barrelLength or 0
    local cosP = math.abs(math.cos(pitch))
    dis = dis - barrelLength * cosP

    local v0 = #properties.velocity == 0 and 0 or tonumber(properties.velocity) / 20
    v0 = v0 and v0 or 0

    local drag = #properties.drag == 0 and 0 or tonumber(properties.drag)
    drag = drag and 1 - drag or 0.99
    
    local result

    if drag < 0.001 or drag > 0.999 then
        result = dis / (cosP * v0)
    else
        result = math.abs(math.log(1 - dis / (100 * (cosP * v0))) / ln(drag))
    end

    return result and result or 0
end

local getY2 = function(t, y0, pitch)
    if t > 10000 then return 0 end
    
    local grav = properties.gravity 
    local sinP = math.sin(pitch)
    local barrelLength = properties.barrelLength or 0
    y0 = barrelLength * sinP + y0
    local v0 = properties.velocity or 0
    v0 = v0 / 20
    local Vy = v0 * sinP
    local drag = properties.drag or 0.01
    drag = 1 - drag
    -- 改进的空气阻力计算
    if drag < 0.001 then
        return y0 + Vy * t - 0.5 * grav * t * t
    else
        local dragFactor = math.pow(drag, t)
        local position = y0 + (Vy * (1 - dragFactor) / (1 - drag))
        position = position - (grav * (t - (1 - dragFactor) / (1 - drag)) / (1 - drag))
        return position
    end
end
local canfire = true
local ag_highsearch = function(xDis, y0, yDis)
    local tolerance = 0.01
    local firstSolution = nil
    for _, pitchRad in ipairs(validPitchList) do
        local time = getTime(xDis, pitchRad)  
        local result = yDis - getY2(time, y0, pitchRad)  
        if math.abs(result) <= 0.5 then  
            firstSolution = { pitch = pitchRad, time = time }
            break  
        end
    end
    canfire = true
    if not firstSolution then
        canfire = false
        local rad0 = math.rad(0)
        return rad0, getTime(xDis, rad0)
    end

    local validMax = validPitchList[#validPitchList]  
    local low = firstSolution.pitch
    local high = validMax  
    local bestPitch, bestTime = firstSolution.pitch, firstSolution.time
    local bestError = math.abs(yDis - getY2(bestTime, y0, bestPitch))

    while (high - low) > tolerance do  
        local midPitch = (low + high) / 2
        local midTime = getTime(xDis, midPitch)
        local midResult = yDis - getY2(midTime, y0, midPitch)
        local midError = math.abs(midResult)

        if midError < bestError then
            bestPitch, bestTime = midPitch, midTime
            bestError = midError
        end

        if midError <= tolerance then
            break  -- 达到精度，提前终止迭代
        end

        if midResult > 0 then
            low = midPitch
        else
            high = midPitch
        end
    end

    return bestPitch, bestTime
end
local function ag_search(arr, xDis, y0, yDis)
    local vertical_diff = yDis - y0
    local target_pitch = math.atan2(vertical_diff, xDis)

    local low_angle = target_pitch
    local start_idx = #arr + 1 
    local low, high = 1, #arr
    while low <= high do
        local mid = math.floor((low + high) / 2)
        if arr[mid] >= low_angle then
            start_idx = mid
            high = mid - 1
        else
            low = mid + 1
        end
    end

    low = start_idx
    high = #arr
    local best_pitch, best_time = nil, nil
    local min_error = math.huge

    while low <= high do
        local mid = math.floor((low + high) / 2)
        local current_pitch = arr[mid]
        
        local current_time = getTime(xDis, current_pitch)
        local current_y = getY2(current_time, y0, current_pitch)
        local error = math.abs(current_y - yDis)

        if error < min_error then
            min_error = error
            best_pitch = current_pitch
            best_time = current_time
        end
        
        if current_y < yDis then
            low = mid + 1
        else
            high = mid - 1
        end
    end

    if best_pitch ~= nil then
        canfire = true
        return best_pitch, best_time
    else
        canfire = false
        return nil, nil
    end
end
local controlCenter = { tgPos = { x = 0, y = 0, z = 0 }, velocity = { x = 0, y = 0, z = 0 }, mode = 2, fire = false }
local ct = 20
local listener = function()
    local controlCenterId = #properties.controlCenterId == 0 and 0 or tonumber(properties.controlCenterId)
    controlCenterId = controlCenterId and controlCenterId or 0
    while true do
        local id, msg = rednet.receive(protocol, 2)
        if properties.camera == false and properties.Auto == false then
            if id == controlCenterId then
                controlCenter = msg
                ct = 20
            end
        else
            ct = 20
            controlCenter = { tgPos = { x = 0, y = 0, z = 0 }, velocity = { x = 0, y = 0, z = 0 }, mode = 2, fire = false }
        end
    end
end

local cross_point = newVec()
local box_1 = peripheral.find("gtceu:lv_super_chest")
box_1 = box_1 and box_1 or peripheral.find("gtceu:mv_super_chest")
box_1 = box_1 and box_1 or peripheral.find("gtceu:hv_super_chest")
box_1 = box_1 and box_1 or peripheral.find("gtceu:ev_super_chest")
local box_2 = peripheral.find("create:depot")
local bullets_count = 0
local getBullets_count = function ()
    while true do
        if box_1 and box_2 then
            local box_1_count, box_2_count = 0, 0
            if box_2 and box_2.getItemDetail(1) then
                box_2_count = box_2.getItemDetail(1).count
            end
            if box_1 and box_1.getItemDetail(1) then
                box_1_count = box_1.getItemDetail(1).count
            end
            bullets_count = box_1_count + box_2_count
        end
        sleep(0.05)
    end
end

local sendRequest = function()
    local slug = ship and ship.getName() or nil
    while true do
        local controlCenterId = #properties.controlCenterId == 0 and 0 or tonumber(properties.controlCenterId)
        controlCenterId = controlCenterId and controlCenterId or 0
        rednet.send(controlCenterId, {
            name = properties.Name,
            pw = properties.password,
            slug = slug,
            bullets_count = bullets_count,
            cross_point = cross_point
        }, request_protocol)
        sleep(0.05)
    end
end

local run_msgSend = function ()
    parallel.waitForAll(sendRequest, getBullets_count)
end

local runListener = function()
    parallel.waitForAll(run_msgSend, listener)
end

local quatList = {
    west  = { w = -1, x = 0, y = 0, z = 0 },
    south = { w = -0.70710678118654752440084436210485, x = 0, y = -0.70710678118654752440084436210485, z = 0 },
    east  = { w = 0, x = 0, y = -1, z = 0 },
    north = { w = -0.70710678118654752440084436210485, x = 0, y = 0.70710678118654752440084436210485, z = 0 },
}
local caquatList = {
    west  = -1.57,
    south = 3.14,
    east  = 1.57,
    north = 0,
}
local cannonUtil = {
    pos = { x = 0, y = 0, z = 0 },
    prePos = { x = 0, y = 0, z = 0 },
    velocity = { x = 0, y = 0, z = 0 },
    preVel = { x = 0, y = 0, z = 0 }
}
function cannonUtil:getAtt()
    self.pos = getCannonPos()
    local v = ship.getVelocity()
    self.velocity = {
        x = v.x / 20,
        y = v.y / 20,
        z = v.z / 20,
    }

    self.quat = quatMultiply(quatList[properties.face], ship.getQuaternion())
end

function cannonUtil:setPreAtt()
    self.prePos = self.pos
    self.preVel = self.velocity
end

function cannonUtil:getNextPos(t)
    return {
        x = self.pos.x + self.velocity.x * t,
        y = self.pos.y + self.velocity.y * t,
        z = self.pos.z + self.velocity.z * t,
    }
end
function cannonUtil:getNextVel(t)
    return {
        x = self.velocity.x + self.preVel.x * t,
        y = self.velocity.y + self.preVel.y * t,
        z = self.velocity.z + self.preVel.z * t,
    }
end
local centerX = 400
local centerY = 300
local set = 1
local function drawHUDContent()
    while true do
        if properties.camera then
            if not ho or not ca then 
                printError("need hologram and camera peripherals")
            else
                if set == 1 then
                    ho.Resize(800, 600)
                    ho.SetClearColor(0x00000000)
                    ho.SetScale(0.025,0.025)
                    ca.setClipRange(400)
                    set = 0
                end
                ho.Clear()
                ho.DrawLine(centerX-300, centerY, centerX-40, centerY, BLACK, 0)  
                ho.DrawLine(centerX+40, centerY, centerX+300, centerY, BLACK, 0)  
                ho.DrawLine(centerX, centerY-200, centerX, centerY-40, BLACK, 0)  
                ho.DrawLine(centerX, centerY+40, centerX, centerY+200, BLACK, 0)  
                ho.DrawLine(centerX-30, centerY, centerX-10, centerY, RETICLE, 0)  
                ho.DrawLine(centerX+10, centerY, centerX+30, centerY, RETICLE, 0)  
                ho.DrawLine(centerX, centerY-30, centerX, centerY-10, RETICLE, 0)  
                ho.DrawLine(centerX, centerY+10, centerX, centerY+30, RETICLE, 0)  
                for i = -maxMils*1.5, maxMils*1.5 do
                    if math.abs(i) > 1 then 
                        local x = centerX + i*milDotSpacing
                        ho.DrawLine(x, centerY, x, centerY+tickLength/2, BLACK, 0)
                        if i % 2 ~= 0 then
                            ho.Text(x-5, centerY+tickLength/2+5, tostring(i), RED, 0)
                        end
                    end
                end
                for i = -maxMils, maxMils do
                    if math.abs(i) > 1 then
                        local y = centerY + i*milDotSpacing
                        ho.DrawLine(centerX, y, centerX+tickLength/2, y, BLACK, 0)
                        if i % 2 == 0 then
                            ho.Text(centerX+tickLength/2+5, y-3, tostring(i), TEXT, 0)
                        end
                    end
                end 
                ho.Fill(centerX-1, centerY-1, 2, 2, RETICLE, 0)
                sleep(600)
            end
        end
        sleep(1)
    end
end
------------------------------------------
local fire = false
local name = "campfire_cosy_smoke"
local function getPD(error, pMax, pMin, dMax, dMin)
    local ratio = 1 - math.min(error / 0.2, 1)  -- 误差0时ratio=1，误差>=0.2时ratio=0
    local p = pMin + (pMax - pMin) * ratio
    local d = dMin + (dMax - dMin) * (1 - ratio)
    return p, d
end
local function normalizeAngle(angle)
    while angle > math.pi do
        angle = angle - 2 * math.pi
    end
    while angle < -math.pi do
        angle = angle + 2 * math.pi
    end
    return angle
end
local runCt = function()
    local Precision = 0.02
    cannonUtil:getAtt()
    local omega = RotateVectorByQuat(negaQ(ship.getQuaternion()), ship.getOmega())
    local yServo, pServo = y, p
    if properties.Exchange then
        yServo, pServo = p, y
    end
    local yawAngle, pitchAngle = yServo.getAngle(), pServo.getAngle()
    if ct > 0 then
        ct = ct - 1
        local forecast = tonumber(properties.forecast) or 16
        local cannonPos = cannonUtil:getNextPos(forecast)
        local target = controlCenter.tgPos
        target.y = target.y + 0.5
        local tgVec = {
            x = target.x - cannonPos.x,
            y = target.y - cannonPos.y,
            z = target.z - cannonPos.z
        }
        local tmpT = math.max(0, (500 - math.sqrt(tgVec.x^2 + tgVec.y^2 + tgVec.z^2)) / 500) * 8
        target.x = target.x + controlCenter.velocity.x * tmpT
        target.y = target.y + controlCenter.velocity.y * tmpT
        target.z = target.z + controlCenter.velocity.z * tmpT
        tgVec = {
            x = target.x - cannonPos.x,
            y = target.y - cannonPos.y,
            z = target.z - cannonPos.z
        }
        local xDis = math.sqrt(tgVec.x^2 + tgVec.z^2)
        local tmpPitch, cTime
        if not properties.curvedfire then
            tmpPitch, cTime = ag_search(pitchList, xDis, 0, tgVec.y)
        else
            tmpPitch, cTime = ag_highsearch(xDis, 0, tgVec.y)
        end
        if tmpPitch == nil then tmpPitch = 0 end
        if cTime == nil then cTime = 0 end
        if cTime > 5 and controlCenter.mode > 2 then
            target.x = target.x + controlCenter.velocity.x * cTime 
            target.y = target.y + controlCenter.velocity.y * cTime
            target.z = target.z + controlCenter.velocity.z * cTime
            tgVec = {x = target.x - cannonPos.x, y = target.y - cannonPos.y, z = target.z - cannonPos.z}
            xDis = math.sqrt(tgVec.x^2 + tgVec.z^2)
            if not properties.curvedfire then
                tmpPitch, cTime = ag_search(pitchList, xDis, 0, tgVec.y)
            else
                tmpPitch, cTime = ag_highsearch(xDis, 0, tgVec.y)
            end
            if tmpPitch == nil then tmpPitch = 0 end
            if cTime == nil then cTime = 0 end
        end
        local _c = math.sqrt(tgVec.x^2 + tgVec.z^2)
        local allDis = math.sqrt(tgVec.x^2 + tgVec.y^2 + tgVec.z^2)
        Precision = getCoyawByDistance(allDis)
        local cosP = math.cos(tmpPitch)
        local tmpVec = (cTime > 5)
            and {
                x = allDis * (tgVec.x / _c) * cosP,
                y = allDis * math.sin(tmpPitch),
                z = allDis * (tgVec.z / _c) * cosP
            }
            or tgVec
        local omegaRot = vector.scale(omega, 1/20)
        local omegaLen = math.sqrt(omegaRot.x^2 + omegaRot.y^2 + omegaRot.z^2)
        omegaLen = math.abs(omegaLen) > math.pi and copysign(math.pi, omegaLen) or omegaLen
        local rot
        if omegaLen ~= 0 then
            local norm = vector.scale(omegaRot, 1/omegaLen)
            local halfTheta = omegaLen / 2
            local sinHTheta = math.sin(halfTheta)
            local omegaQuat = {
                w = math.cos(halfTheta),
                x = norm.x * sinHTheta,
                y = norm.y * sinHTheta,
                z = norm.z * sinHTheta
            }
            local count = math.floor(forecast / 4)
            local nextQ = ship.getQuaternion()
            for i = 1, count do
                nextQ = quatMultiply(nextQ, omegaQuat)
            end
            rot = RotateVectorByQuat(quatMultiply(quatList[properties.face], negaQ(nextQ)), tmpVec)
        else
            rot = RotateVectorByQuat(quatMultiply(quatList[properties.face], negaQ(ship.getQuaternion())), tmpVec)
        end
        local tmpYaw = math.atan2(rot.z, -rot.x)
        if properties.lock_yaw_range ~= "0" then
            local limitBaseYaw = caquatList[properties.lock_yaw_face] or 0  
            local lock_yaw_range = math.rad(tonumber(properties.lock_yaw_range) or 0)  
            local yawUpperBound = limitBaseYaw + lock_yaw_range  
            local yawLowerBound = limitBaseYaw - lock_yaw_range  
            yawUpperBound = normalizeAngle(yawUpperBound)
            yawLowerBound = normalizeAngle(yawLowerBound)
            if yawLowerBound < yawUpperBound then
                if tmpYaw < yawLowerBound then
                    tmpYaw = yawLowerBound
                    canfire = false  -- 超出下限，禁止开火
                elseif tmpYaw > yawUpperBound then
                    tmpYaw = yawUpperBound
                    canfire = false  -- 超出上限，禁止开火
                end
            else
                if tmpYaw > yawUpperBound and tmpYaw < yawLowerBound then
                    local distToLower = math.abs(normalizeAngle(tmpYaw - yawLowerBound))
                    local distToUpper = math.abs(normalizeAngle(tmpYaw - yawUpperBound))
                    if distToLower < distToUpper then
                        tmpYaw = yawLowerBound
                    else
                        tmpYaw = yawUpperBound
                    end
                    canfire = false  -- 超出范围，禁止开火
                end
            end
        end

        local setPitch = math.asin(rot.y / math.sqrt(rot.x^2 + rot.y^2 + rot.z^2))
        local minPitchRad = math.rad(tonumber(properties.minPitchAngle) or 45)
        local maxPitchRad = math.rad(90)
        if properties.Lower_hanging then maxPitchRad = math.rad(0) if properties.minPitchAngle > math.rad(0) then properties.minPitchAngle = math.rad(0) end end
        if setPitch < minPitchRad then
            setPitch = minPitchRad
            canfire = false  -- 超出下限，禁止开火
        elseif setPitch > maxPitchRad then
            setPitch = maxPitchRad
            canfire = false  -- 超出上限，禁止开火
        end
        if properties.InvertPitch then setPitch = -setPitch end
        if properties.InvertYaw then tmpYaw= -tmpYaw end
        setPitch = setPitch + math.rad(properties.Cannonpitch)
        local yawError = math.abs(tmpYaw - yawAngle)
        local pitchError = math.abs(setPitch - pitchAngle)
        if yawError < 0.2 then
            local p, d = getPD(yawError, 26, 16, 20, 10)
            yServo.setPID(p, 0, d)
        else
            yServo.setPID(26, 0, 10)
        end

        if pitchError < 0.2 then
            local p, d = getPD(pitchError, 20, 14, 20, 14)
            pServo.setPID(p, 0, d)
        else
            pServo.setPID(20, 0, 14)
        end
        if properties.camera == false then
            if properties.curvedfire then Precision = Precision / 1.25 end
            if math.abs(tmpYaw - yawAngle) < Precision and math.abs(setPitch - pitchAngle) < Precision  then
                fire = controlCenter.fire
            else
                fire = false
            end
            if not canfire then fire = false end
        else
            if math.abs(tmpYaw - yawAngle) < Precision / 2 and math.abs(setPitch - pitchAngle) < Precision / 2  then
                ho.Text(centerX + 60, centerY + 120, string.format("Cannon Ok!"), RED, 0)
            end
        end
        yServo.setTargetValue(tmpYaw) 
        pServo.setTargetValue(setPitch)
        name = "campfire_cosy_smoke"
        if fire and commands then genHitPredictionCircle(target.x, target.y, target.z, 50, 2, 0.5, name) end
    else
        local cannonPitch = tonumber(properties.Cannonpitch)
        local cannonYAW = tonumber(properties.CannonYAW)
        local yawVal = math.rad(cannonYAW or 0)
        local pitchVal = math.rad(cannonPitch or 0)
        if cannonPitch and cannonPitch > 180 then cannonPitch = cannonPitch - 360 end
        if cannonYAW and cannonYAW > 180 then cannonYAW = cannonYAW - 360 end
        yServo.setTargetValue(yawVal)
        pServo.setTargetValue(pitchVal)
        yServo.unlock(true) 
        pServo.unlock(true)
    end
end
---------------------------
local Autoset = true
local see = function ()
    local last = nil
    while true do
        if properties.camera then
            local current = ca.getYaw() + ca.getPitch()
            if last ~= nil and current == last then
                ho.Fill(centerX + 60,  centerY + 40,  centerX +80,  centerY + 120 ,  0x00000000 ,0)
                ca.clipNewBlock()
                ca.clipNewEntity()
                ca.clipNewShip()
                local cameraPos = ca.getCameraPosition()
                local candidates = {}
                local ship = ca.latestShip()
                local entity = ca.latestEntity()
                local block = ca.latestBlock()
                if ship and ship.hit then
                    local dx = ship.hit.x - cameraPos.x
                    local dy = ship.hit.y - cameraPos.y
                    local dz = ship.hit.z - cameraPos.z
                    local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
                    table.insert(candidates, {obj = ship, dist = dist})
                end
                if entity and entity.hit then
                    local dx = entity.hit.x - cameraPos.x
                    local dy = entity.hit.y - cameraPos.y
                    local dz = entity.hit.z - cameraPos.z
                    local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
                    table.insert(candidates, {obj = entity, dist = dist})
                end
                if block and block.hit then
                    local dx = block.hit.x - cameraPos.x
                    local dy = block.hit.y - cameraPos.y
                    local dz = block.hit.z - cameraPos.z
                    local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
                    table.insert(candidates, {obj = block, dist = dist})
                end
                local result = nil
                local minDist = math.huge
                for _, v in ipairs(candidates) do
                    if v.dist < minDist then
                        minDist = v.dist
                        result = v.obj
                    end
                end
                if result then
                    if result == ship then
                        ho.Text(centerX + 60, centerY + 100, string.format("It Is a Ship!"), RED, 0)
                    end
                    local dx = result.hit.x - cameraPos.x
                    local dy = result.hit.y - cameraPos.y
                    local dz = result.hit.z - cameraPos.z
                    local D = math.sqrt(dx*dx + dy*dy + dz*dz)
                    if D < 5 then 
                        if entity and entity.hit then
                            result = entity
                        elseif block and block.hit then
                            result = block
                        end
                        dx = result.hit.x - cameraPos.x
                        dy = result.hit.y - cameraPos.y
                        dz = result.hit.z - cameraPos.z
                        D = math.sqrt(dx*dx + dy*dy + dz*dz)
                    end
                    local pitch = -ca.getPitch()
                    local yaw = normalizeAngle((ca.getYaw() + caquatList[properties.face] + properties.CannonYAW))
                    if yaw > 180 then yaw = yaw - 360 end
                    ho.Text(centerX+60, centerY+40, string.format("AZ: %.1f°", yaw), TEXT, 0)
                    ho.Text(centerX+60, centerY+60, string.format("EL: %.1f°", pitch), TEXT, 0)
                    ho.Text(centerX + 60, centerY + 80, string.format("D: %.1f°", D), ORANGE, 0)
                    if result.type ~= nil then
                        local lastDotIndex = result.type:find("[^%.]*$")
                        local lastPart = result.type:sub(lastDotIndex)
                        ho.Text(centerX + 60, centerY + 100, string.format("type: %s", lastPart), RED, 0)
                    end
                    ho.Flush()
                    if properties.Auto then
                        controlCenter.tgPos.x = result.hit.x
                        controlCenter.tgPos.y = result.hit.y
                        controlCenter.tgPos.z = result.hit.z
                        Autoset = false
                        runCt()
                    else
                        Autoset = true
                    end
                    name = "dripping_lava"
                    if commands then genHitPredictionCircle(result.hit.x, result.hit.y, result.hit.z, 30, 2, 0.5, name) end
                end
            end
            last = current
        end
        sleep(0.5)
    end
end
local function drawView()
    local yServo, pServo = y, p
    local cannonPitch = tonumber(properties.Cannonpitch)
    if properties.Exchange then
        yServo, pServo = p, y
    end
    local yawAngle, pitchAngle = yServo.getAngle(), pServo.getAngle()
    local currentPitch = -ca.getPitch() or 0
    local currentYaw = -ca.getYaw() or 0
    local setcurrentYaw = currentYaw % 360
    local relX, relY, relZ = calculateRelativePosition(currentYaw, currentPitch)  
    ho.SetTranslation(relX, relY, relZ)
    ho.SetRotation(currentYaw+180, currentPitch, 0)
    if properties.InvertYaw then setcurrentYaw = -setcurrentYaw end
    if properties.InvertPitch then currentPitch = -currentPitch end
    if properties.Auto == false and Autoset == true then 
        pServo.setTargetValue(math.rad(currentPitch + (cannonPitch or 0))) 
        yServo.setTargetValue(math.rad(setcurrentYaw) + caquatList[properties.face])
        if math.abs(math.rad(setcurrentYaw) + caquatList[properties.face] - yawAngle) < 0.3 then
            yServo.setPID(22,0,18)
        else
            yServo.setPID(24,0,14)
        end
        if math.abs(math.rad(currentPitch + (cannonPitch or 0)) - pitchAngle) < 0.2 then
            pServo.setPID(20,0,18)
        else
            pServo.setPID(20,0,14)
        end
    end
end
---------------------------
local termUtil = {
    cpX = 1,
    cpY = 1,
}
local absTextField = {
    x = 1,
    y = 1,
    len = 15,
    text = "",
    textCorlor = "0",
    backgroundColor = "8",
}

function absTextField:paint()
    local str = ""
    for i = 1, self.len, 1 do
        local text = tostring(self.key[self.value])
        local tmp = string.sub(text, i, i)
        if #tmp > 0 then
            str = str .. tmp
        else
            local tmp2 = ""
            for j = 0, self.len - i, 1 do
                tmp2 = tmp2 .. " "
            end
            str = str .. tmp2
            break
        end
    end

    term.setCursorPos(self.x, self.y)
    term.blit(str, genStr(self.textCorlor, #str), genStr(self.backgroundColor, #str))
end

function absTextField:inputChar(char)
    local xPos, yPos = term.getCursorPos()
    xPos = xPos + 1 - self.x
    local field = tostring(self.key[self.value])
    if #field < self.len then
        if self.type == "number" then
            if char >= '0' and char <= '9' then
                if field == "0" then
                    field = char
                else
                    field = string.sub(field, 1, xPos) .. char .. string.sub(field, xPos, #field)
                end

                self.key[self.value] = tonumber(field)
                termUtil.cpX = termUtil.cpX + 1
            end
            if char == '-' then
                self.key[self.value] = -self.key[self.value]
            end
        elseif self.type == "string" then
            local strEnd = string.sub(field, xPos, #field)
            field = string.sub(field, 1, xPos - 1) .. char .. strEnd
            self.key[self.value] = field
            termUtil.cpX = termUtil.cpX + 1
        end
    end
end

function absTextField:inputKey(key)
    local xPos, yPos = term.getCursorPos()
    local field = tostring(self.key[self.value])
    local minXp = self.x
    local maxXp = minXp + #field
    if key == 259 or key == 261 then 
        if xPos > minXp then
            termUtil.cpX = termUtil.cpX - 1
            if #field > 0 and termUtil.cpX > 1 then
                local index = termUtil.cpX - self.x
                field = string.sub(field, 1, index) .. string.sub(field, index + 2, #field)
            end
            if self.type == "number" then
                local number = tonumber(field)
                if not number then
                    self.key[self.value] = 0
                else
                    self.key[self.value] = number
                end
            elseif self.type == "string" then
                self.key[self.value] = field
            end
        end
    elseif key == 257 or key == 335 then
    elseif key == 262 or key == 263 then
        if key == 262 then
            termUtil.cpX = termUtil.cpX + 1
        elseif key == 263 then
            termUtil.cpX = termUtil.cpX - 1
        end
    elseif key == 264 or key == 258 then
    elseif key == 265 then
    end
    termUtil.cpX = termUtil.cpX > maxXp and maxXp or termUtil.cpX
    termUtil.cpX = termUtil.cpX < minXp and minXp or termUtil.cpX
end


function absTextField:click(x, y)
    local xPos = self.x
    if x >= xPos then
        if x < xPos + #tostring(self.key[self.value]) then
            termUtil.cpX, termUtil.cpY = x, y
        else
            termUtil.cpX, termUtil.cpY = xPos + #tostring(self.key[self.value]), y
        end
    end
end

local newTextField = function(key, value, x, y)
    return setmetatable({ key = key, value = value, type = type(key[value]), x = x, y = y },
        { __index = absTextField })
end

local absSelectBox = {
    x = 1,
    y = 1,
    label = "",
    contents = {},
    count = 0,
    interval = 0,
    fontColor = "8",
    backgroundColor = "f",
    selectColor = "e"
}

function absSelectBox:paint()
    term.setCursorPos(self.x, self.y)
    local select = tostring(self.key[self.value])
    for i = 1, #self.contents, 1 do
        local str = tostring(self.contents[i])
        if select == str then
            term.blit(str, genStr(self.backgroundColor, #str), genStr(self.selectColor, #str))
        else
            term.blit(str, genStr(self.fontColor, #str), genStr(self.backgroundColor, #str))
        end
        for j = 1, self.interval, 1 do
            term.write(" ")
        end
    end
end

function absSelectBox:click(x, y)
    local xPos = x + 1 - self.x
    local index = 0
    for i = 1, #self.contents, 1 do
        if xPos >= index and xPos <= index + #tostring(self.contents[i]) then
            self.key[self.value] = self.contents[i]
            break
        end
        index = index + #tostring(self.contents[i]) + self.interval
    end
end

local newSelectBox = function(key, value, interval, x, y, ...)
    return setmetatable(
        { key = key, value = value, interval = interval, x = x, y = y, type = type(key[value]), contents = { ... } },
        { __index = absSelectBox })
end

local absSlider = {
    x = 1,
    y = 1,
    min = 0,
    max = 1,
    len = 20,
    fontColor = "8",
    backgroundColor = "f"
}

function absSlider:paint()
    local field = self.key[self.value]
    if field == "-" then
        field = 0
    end
    local maxVal = self.max - self.min
    local xPos = math.floor((field - self.min) * (self.len / maxVal) + 0.5)
    xPos = xPos < 1 and 1 or xPos
    term.setCursorPos(self.x, self.y)
    for i = 1, self.len, 1 do
        if xPos == i then
            term.blit(" ", self.backgroundColor, self.fontColor)
        else
            term.blit("-", self.fontColor, self.backgroundColor)
        end
    end
end

function absSlider:click(x, y)
    local xPos = x + 1 - self.x
    if xPos > self.len then
        xPos = self.len
    end
    self.key[self.value] = math.floor((self.max - self.min) * (xPos / self.len) + 0.5) + self.min
end

local newSlider = function(key, value, min, max, len, x, y)
    return setmetatable({ key = key, value = value, min = min, max = max, len = len, x = x, y = y },
        { __index = absSlider })
end

local selfId = os.getComputerID()
local runTerm = function()
    local fieldTb = {
        velocity = newTextField(properties, "velocity", 11, 2),
        barrelLength = newTextField(properties, "barrelLength", 30, 2),
        minPitchAngle = newTextField(properties, "minPitchAngle", 17, 8),
        lock_yaw_range = newTextField(properties, "lock_yaw_range", 20, 12),
        cannonOffset_x = newTextField(properties.cannonOffset, "x", 18, 6),
        cannonOffset_y = newTextField(properties.cannonOffset, "y", 25, 6),
        cannonOffset_z = newTextField(properties.cannonOffset, "z", 32, 6),
        gravity = newTextField(properties, "gravity", 44, 6),
        drag = newTextField(properties, "drag", 44, 7),
        forecast = newTextField(properties, "forecast", 45, 2),
        Name = newTextField(properties, "Name", 8, 17),
        controlCenterId = newTextField(properties, "controlCenterId", 19, 18),
        password = newTextField(properties, "password", 37, 18),
    }
    fieldTb.velocity.len = 5
    fieldTb.barrelLength.len = 3
    fieldTb.controlCenterId.len = 5
    fieldTb.password.len = 14
    fieldTb.minPitchAngle.len = 5
    fieldTb.lock_yaw_range.len = 5
    fieldTb.cannonOffset_x.len = 3
    fieldTb.cannonOffset_y.len = 3
    fieldTb.cannonOffset_z.len = 3
    fieldTb.gravity.len = 5
    fieldTb.drag.len = 5
    fieldTb.forecast.len = 5
    local selectBoxTb = {
        INPUT = newSelectBox(properties, "INPUT", 2, 8, 3, "top", "left", "right", "front", "back","bottom"),
        face = newSelectBox(properties, "face", 2, 8, 5, "south", "west", "north", "east"),
        lock_yaw_face = newSelectBox(properties, "lock_yaw_face", 2, 27, 12, "south", "west", "north", "east"),
        InvertYaw = newSelectBox(properties, "InvertYaw", 1, 41, 13, false, true),
        InvertPitch = newSelectBox(properties, "InvertPitch", 1, 15, 13, false, true),
        Exchange = newSelectBox(properties, "Exchange", 2, 14, 14, false, true),
        CannonYAW = newSelectBox(properties, "CannonYAW", 2, 14, 15, "0", "90", "180", "270"),
        Cannonpitch = newSelectBox(properties, "Cannonpitch", 2, 14, 16, "0", "90","-90"),
        fire = newSelectBox(properties, "fire", 2, 8, 4, "top", "left", "right", "front", "back"),
        curvedfire = newSelectBox(properties, "curvedfire", 1, 41, 14, false, true),
        camera = newSelectBox(properties, "camera", 1, 41, 15, false, true),
        Auto = newSelectBox(properties, "Auto", 1, 41, 16, false, true),
        Lower_hanging = newSelectBox(properties, "Lower_hanging", 1, 25, 8, false, true)
    }

    local sliderTb = {
        minPitchAngle = newSlider(properties, "minPitchAngle", -90, 90, 49, 2, 9)
    }
    local alarm_flag = false
    term.clear()
    term.setCursorPos(15, 8)
    term.write("Press any key to continue")
    while true do
        local eventData = { os.pullEvent() }
        local event = eventData[1]
        if event == "mouse_up" or event == "key_up" or event == "alarm"
            or event == "mouse_click" or event == "mouse_drag" or event == "key" or event == "char" then
            if not alarm_flag then
                alarm_flag = true
            else
                term.clear()
                term.setCursorPos(18, 1)
                printError(string.format("self id: %d", selfId))
                term.setCursorPos(2, 2)
                term.write("Velocity")
                term.setCursorPos(17, 2)
                term.write("BarrelLength")
                term.setCursorPos(36, 2)
                term.write("forecast:")

                term.setCursorPos(2, 3)
                term.write("INPUT: ")
                term.setCursorPos(2, 4)
                term.write("FIRE: ")

                term.setCursorPos(2, 6)
                term.write("CannonOffset: x=     y=     z=")
                term.setCursorPos(36, 6)
                term.write("gravity: ")
                term.setCursorPos(36, 7)
                term.write("   drag: ")
                term.setCursorPos(2, 8)
                term.write("MinPitchAngle: ")
                term.setCursorPos(2, 12)
                term.write("lock_yaw_range: +-")
                term.setCursorPos(2, 5)
                term.write("Face: ")
                term.setCursorPos(2, 13)
                term.write("InvertPitch: ")
                term.setCursorPos(30, 13)
                term.write("InvertYaw: ")
                term.setCursorPos(30, 14)
                term.write("curvedfire: ")
                term.setCursorPos(2, 14)
                term.write("Exchange:")
                term.setCursorPos(2, 15)
                term.write("Cannonyaw:")
                term.setCursorPos(2, 16)
                term.write("Cannonpitch:")
                term.setCursorPos(34, 15)
                term.write("camera:")
                term.setCursorPos(34, 16)
                term.write("Auto:")
                term.setCursorPos(2, 17)
                term.write("Name: ")
                term.setCursorPos(2, 18)
                term.write("controlCenterId: ")
                term.setCursorPos(27, 18)
                term.write("Password: ")
                
                for k, v in pairs(fieldTb) do
                    v:paint()
                end

                for k, v in pairs(selectBoxTb) do
                    v:paint()
                end

                for k, v in pairs(sliderTb) do
                    v:paint()
                end

                term.setCursorPos(termUtil.cpX, termUtil.cpY)

                if event == "mouse_click" then
                    term.setCursorBlink(true)
                    local x, y = eventData[3], eventData[4]
                    for k, v in pairs(fieldTb) do
                        if y == v.y and x >= v.x and x <= v.x + v.len then
                            v:click(x, y)
                        end
                    end
                    for k, v in pairs(fieldTb) do 
                        if y == v.y and x >= v.x and x <= v.x + v.len then
                            v:click(x, y)
                        end
                    end
                    for k, v in pairs(selectBoxTb) do 
                        if y == v.y then
                            v:click(x, y)
                        end
                    end
                    for k, v in pairs(sliderTb) do
                        if y == v.y then
                            v:click(x, y)
                        end
                    end
                elseif event == "mouse_drag" then
                    local x, y = eventData[3], eventData[4]
                    for k, v in pairs(sliderTb) do
                        if y == v.y then
                            v:click(x, y)
                        end
                    end
                elseif event == "key" then
                    local key = eventData[2]
                    for k, v in pairs(fieldTb) do
                        if termUtil.cpY == v.y and termUtil.cpX >= v.x and termUtil.cpX <= v.x + v.len then
                            v:inputKey(key)
                        end
                    end
                elseif event == "char" then
                    local char = eventData[2]
                    for k, v in pairs(fieldTb) do
                        if termUtil.cpY == v.y and termUtil.cpX >= v.x and termUtil.cpX <= v.x + v.len then
                            v:inputChar(char)
                        end
                    end
                end
                system.write(system.fileName, properties)
            end
        end
    end
end

local checkFire = function()
    while true do
        if fire and ct > 0 or redstone.getInput(properties.INPUT) == true then
            y.lock(true) 
            p.lock(true)
            if not redstone.getOutput(properties.fire) then
                redstone.setOutput(properties.fire, true)
            end
            sleep(0.3)
            redstone.setOutput(properties.fire, false)
            y.unlock(true) 
            p.unlock(true)
            sleep(0.2)
        else
            if redstone.getOutput(properties.fire) then
                redstone.setOutput(properties.fire, false)
            y.unlock(true) 
            p.unlock(true)
            end
        end
        sleep(0.05)
    end
end

local type_run = function ()
  while true do
    if properties.camera == false then
    runCt()
    sleep(0.1) 
    else
    drawView()
    sleep(0.05) 
    end
end  
end

local run = function()
    parallel.waitForAll(runTerm,checkFire,drawHUDContent,see)
end

parallel.waitForAll(run, runListener,type_run)